-
Notifications
You must be signed in to change notification settings - Fork 11.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[10.x] Fix "after commit" callbacks not running on nested transactions #48466
[10.x] Fix "after commit" callbacks not running on nested transactions #48466
Conversation
8f0232a
to
bf2764b
Compare
I've removed the test I added here because the build was failing (even though the test passes locally in isolation - the suite also fails locally when running it entirely). I added that test because I wanted a way to test the framework as an application test was using it. Since this issue only appears in tests, I'm unsure if this is the right way. Ignoring one transaction level was a good solution because only the test code was added to the transition to the ignore list. And now we're calling that inside the framework itself... 🤔 |
I marked it as ready for review so I could get some feedback:
|
Signed-off-by: Mior Muhammad Zaki <[email protected]>
Signed-off-by: Mior Muhammad Zaki <[email protected]>
@tonysm added tests based on your example as Integration tests |
Signed-off-by: Mior Muhammad Zaki <[email protected]>
After careful reconsideration, I believe this corrective approach is incorrect. I consider the following logic to be highly suspect: if ($this->transactions == 1) {
$this->fireConnectionEvent('committing');
$this->getPdo()->commit();
}
$this->transactions = max(0, $this->transactions - 1);
if ($this->afterCommitCallbacksShouldBeExecuted()) {
$this->transactionsManager?->commit($this->getName());
} The root cause of the inconsistency is that we are only decrementing I'm now preparing a new PR, testing to see if this conjecture is correct. |
@crynobone thank you! |
Co-authored-by: Mior Muhammad Zaki <[email protected]>
I'm starting to worry we are digging a deeper and deeper complicated hole for ourselves after the first |
Hi @taylorotwell, Indeed,
As a long-term solution, I started trying to fix it with the approach in #48471. However, the changes seem to be getting quite complex, and I'm at my wit's end. The problem is becoming too challenging for me to handle. I'd like to request someone else to create a new PR and continue the work 😢 |
@taylorotwell I feel you. I agree with @mpyw. I think But in this case, the issue seems to be with test transactions and the This is not an issue in production apps. It only affects testing things that use I've turned the PR back to draft. I have an idea of how this can be fixed: we could have a test-specific transaction manager that would not count the test wrapping transaction when deciding if the callbacks should run. |
to avoid remembering which transactions to ignore Before, we were remembering the test transaction so we could ignore it when deciding to run the after commit callbacks or not. We're still handling the after commit callbacks like that but now instead of remembering which transactions to ignore, we're always calling the DatabaseTransactionManager::commit method. The difference is that now we're passing the current transaction level to it. The method will decide to call the callbacks or not based on that level and whether or not this is on in test mode. When in tests, instead of setting the current transaction to be remembered so it could be ignored, we're now only setting the DatabaseTransactionManager to test mode. When in test mode, it will execute the callbacks when the transactions count reaches 1 (remember that the test runs in a transaction, so that's the "root" level). Otherwise, it runs the callbacks when the transactions level is on level 0 (like in production). There's also a change in the DatabaseTransactionManager::addCallback method. It now also checks if it's in test mode. When not in test mode, it only adds the callback to the execution queue if there's an open transaction. Otherwise, the callback is executed right away. When in test mode, the number of transactions has to be greater than one for it to be added to the callbacks queue.
I agree with that! |
createOrFirst
not running callbacks on tests
Ready for review. \cc @taylorotwell @crynobone @mpyw |
@mpyw what are your thoughts on the overall state of this PR now? |
While I believe it's okay to merge in terms of functionality, there might be room for renaming and structural refactoring. I'd like to leave the final decision up to you. My thoughts: Rename
|
Like this:
|
After giving it some thought, I've come to believe that just renaming the method might be sufficient, considering the feasibility. I think the method name |
I'll rename the method in a bit. |
* | ||
* @return \Illuminate\Support\Collection | ||
*/ | ||
public function callbackApplicableTransactions() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one too, should I add back the signature? We could probably implement it to return all transactions when not in test mode and, if in test mode, skip the first one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added this method back with an implementation I believe should work the same as the previous one. We're also marking the method as deprecated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
By the way, if we get one more issue related to |
I understand your concern, but I really hope that there won't be any more serious issues. The bug this time was due to a lack of consideration on the I feel this change has benefited many applications. It even obviated the need for a third-party library I used to operate with, so I hope the results of this modification will persist. |
I also see the benefits of using it in the other methods. It's just that it had some unexpected side effects, unfortunately. No blaming. Just wanted to say that we have that as an option if we have to. |
Closing it as @crynobone has an improved version with distinct |
Changed
Illuminate\Database\DatabaseTransactionsManager::callbackApplicableTransactions
method (no longer used in the framework)Illuminate\Database\DatabaseTransactionsManager::callbacksShouldIgnore
method (no longer used in the framework. We're now using the newwithAfterCommitCallbacksInTestTransactionAwareMode
method, which this one is forwarding toIlluminate\Database\DatabaseTransactionsManager::withAfterCommitCallbacksInTestTransactionAwareMode
method meant to be used by the test database traits when they start a transaction. This should make sure that we execute the callbacks properly at the right level of the transaction (since the tests using the database traits start a wrapping transaction and never actually commit)Illuminate\Database\DatabaseTransactionsManager::commit($connection, $level = 1)
method. Now, instead of only having to pass the connection name, callers should also pass the transaction level. It's based on that level (and the fact that we're in test mode or not) that we're going to decide if the callbacks should run or not). All calls within the framework were updated.Illuminate\Database\DatabaseTransactionsManager::commit($connection, $level = 1)
is now called everytime there's a commit. Before it was only being called on the "last one", but now it receives the transaction level and it should decide if the callbacks should be triggered or not.A More In-Depth Explanation
Changes the way "after commit" callbacks are executed to avoid
remembering which transactions to ignore.
Before, we remembered the test transaction, so we
could ignore it when deciding to run the "after commit"
callbacks or not.
We're still handling the "after commit" callbacks like that,
but now, instead of remembering which transactions to ignore,
we're always calling the
DatabaseTransactionManager::commit
method. The difference is that now we're passing the current
transaction level to it. The method will decide whether to run the
callbacks based on that level and whether or not this
is in test mode.
When in tests, instead of setting the current transaction to be
remembered so it could be ignored, we're now only setting the
DatabaseTransactionManager to test mode. When in test mode, it
will execute the callbacks when the transaction count reaches 1
(remember that the test runs in a transaction, so that's the
"root" level). Otherwise, it runs the callbacks when the transaction
level is on level 0 (like in production).
There's also a change in the
DatabaseTransactionManager::addCallback
method. It now also checks if it's in test mode. When not in test mode,
it only adds the callback to the execution queue if there's an open
transaction. Otherwise, the callback is executed right away. When in
test mode, the number of transactions has to be greater than one for
it to be added to the callbacks queue.
Context
With the addition of the
withSavepointIfNeeded
in #48144, we added a new transition level that affects tests when you're using "after commit" callbacks and nesting transactions. This wasn't an issue before because we generally have one transaction wrapping. When thecreateOrFirst
was added to other*OrCreate
methods (firstOrCreate
andupdateOrCreate
), this issue became more apparent.fixes #48451